#本代码的作用需求是视频mp4里的人说的话，例如培训人员的讲话，提取文字出来
#之前的手动操做，分两步，第一步用狸窝万能转换器转换mp4到音频mp3文件，第二步，把mp3通过夸克提取文字出来，但是有数巨泄露风险
#目前改造，1，通过ffmpg把mp4文件， 按照静音的时间，分拆， 并提交到内部GPUSTACK的语音识别大模型，识别后，回传到GUI界面
import subprocess
import re
import os
import requests
import json
from datetime import datetime
import tkinter as tk
from tkinter import filedialog, scrolledtext, messagebox, ttk
import threading
import sys

# 隐藏控制台窗口（仅在Windows系统有效）
if os.name == 'nt':
    try:
        import ctypes
        ctypes.windll.user32.ShowWindow(ctypes.windll.kernel32.GetConsoleWindow(), 0)
    except:
        pass

class MP4ToTextConverter:
    def __init__(self, root):
        self.root = root
        self.root.title("MP4视频转文字工具")
        self.root.geometry("800x600")
        self.root.resizable(True, True)
        
        # 配置参数
        self.input_file = ""  # 输入的视频文件路径
        self.output_dir = "output"     # 输出目录
        self.noise_threshold = "-30dB" # 静音阈值
        self.silence_duration = 2      # 最小静音时长(秒)
        self.max_segment_duration = 180 # 最大片段时长(秒)
        self.ffmpeg_path = r".\ffmpeg\ffmpeg.exe"  # ffmpeg可执行文件路径
        
        # GPustack API配置
        self.api_url = "http://bjg4gpuskv1p.accounts.intern/v1"
        self.model_name = "sensevoicesmall"
        self.api_key = "gpustack_你猜猜这里是什么"
        self.api_endpoint = f"{self.api_url}/audio/transcriptions"
        
        # 处理状态
        self.processing = False
        
        # 初始化GUI组件
        self.result_text = None  # 先声明变量
        self.create_widgets()    # 调用创建组件的方法
    
    def create_widgets(self):
        # 创建顶部框架用于文件选择
        top_frame = ttk.Frame(self.root, padding="10")
        top_frame.pack(fill=tk.X)
        
        ttk.Label(top_frame, text="选择MP4视频文件:").pack(side=tk.LEFT, padx=5)
        
        self.file_path_var = tk.StringVar()
        ttk.Entry(top_frame, textvariable=self.file_path_var, width=50).pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)
        
        ttk.Button(top_frame, text="浏览...", command=self.browse_file).pack(side=tk.LEFT, padx=5)
        ttk.Button(top_frame, text="开始处理", command=self.start_processing).pack(side=tk.LEFT, padx=5)
        
        # 创建进度条
        self.progress_frame = ttk.Frame(self.root, padding="0 0 10 10")
        self.progress_frame.pack(fill=tk.X, padx=10)
        
        self.progress_var = tk.DoubleVar()
        self.progress_bar = ttk.Progressbar(self.progress_frame, variable=self.progress_var, maximum=100)
        self.progress_bar.pack(fill=tk.X, expand=True)
        
        self.status_var = tk.StringVar(value="就绪")
        ttk.Label(self.progress_frame, textvariable=self.status_var).pack(anchor=tk.W)
        
        # 创建结果显示区域
        ttk.Label(self.root, text="识别结果:").pack(anchor=tk.W, padx=10)
        
        # 使用frame和padx/pady代替padding参数
        result_frame = ttk.Frame(self.root, padding="10")
        result_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
        
        # 初始化result_text组件（不使用padding参数）
        self.result_text = scrolledtext.ScrolledText(result_frame, wrap=tk.WORD)
        self.result_text.pack(fill=tk.BOTH, expand=True)
        self.result_text.config(state=tk.DISABLED)
    
    def browse_file(self):
        file_path = filedialog.askopenfilename(
            title="选择MP4视频文件",
            filetypes=[("MP4文件", "*.mp4"), ("所有文件", "*.*")]
        )
        if file_path:
            self.file_path_var.set(file_path)
            self.input_file = file_path
    
    def start_processing(self):
        if self.processing:
            messagebox.showinfo("提示", "正在处理中，请稍候...")
            return
            
        self.input_file = self.file_path_var.get()
        if not self.input_file or not os.path.exists(self.input_file):
            messagebox.showerror("错误", "请选择有效的MP4文件")
            return
            
        if not self.check_ffmpeg():
            messagebox.showerror("错误", f"未找到ffmpeg程序，路径：{self.ffmpeg_path}")
            return
            
        # 清空之前的结果
        if self.result_text:  # 增加检查，确保组件已初始化
            self.result_text.config(state=tk.NORMAL)
            self.result_text.delete(1.0, tk.END)
            self.result_text.config(state=tk.DISABLED)
        else:
            messagebox.showerror("错误", "界面组件初始化失败")
            return
        
        # 开始处理（在新线程中，避免GUI冻结）
        self.processing = True
        self.status_var.set("正在准备处理...")
        self.progress_var.set(0)
        
        # 禁用按钮
        for widget in self.root.winfo_children():
            if isinstance(widget, ttk.Button) and widget["text"] in ["开始处理", "浏览..."]:
                widget.config(state=tk.DISABLED)
        
        # 启动处理线程
        threading.Thread(target=self.process_audio_sequentially, daemon=True).start()
    
    def check_ffmpeg(self):
        """检查ffmpeg是否存在并可用"""
        if not os.path.exists(self.ffmpeg_path):
            return False
        
        try:
            # 检查ffmpeg版本，验证是否可用，隐藏控制台窗口
            subprocess.run(
                [self.ffmpeg_path, "-version"],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
                creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
            )
            return True
        except Exception as e:
            return False
    
    def get_split_points(self, input_file):
        """获取所有可能的分割点（仅分析不切割）"""
        self.update_status("正在分析音频，寻找分割点...")
        
        ffmpeg_cmd = [
            self.ffmpeg_path, "-i", input_file, "-vn",
            "-af", f"silencedetect=noise={self.noise_threshold}:d={self.silence_duration}",
            "-f", "null", "-"
        ]
        
        # 执行ffmpeg命令时隐藏控制台窗口
        result = subprocess.run(
            ffmpeg_cmd,
            stderr=subprocess.PIPE,
            stdout=subprocess.PIPE,
            text=True,
            creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
        )
        
        # 解析静音事件
        silence_pattern = re.compile(r"silence_(start|end): (\d+\.\d+)")
        silence_events = []
        for line in result.stderr.splitlines():
            match = silence_pattern.search(line)
            if match:
                event_type = match.group(1)
                time = float(match.group(2))
                silence_events.append((event_type, time))
        
        # 提取分割点
        split_points = [0.0]
        for i, (event_type, time) in enumerate(silence_events):
            if event_type == "end" and i > 0 and silence_events[i-1][0] == "start":
                start_time = silence_events[i-1][1]
                if time - start_time >= self.silence_duration:
                    split_points.append(time)
        
        # 过滤分割点，确保不超过最大时长
        filtered_split_points = [0.0]
        for point in split_points[1:]:
            if point - filtered_split_points[-1] > self.max_segment_duration:
                filtered_split_points.append(filtered_split_points[-1] + self.max_segment_duration)
            filtered_split_points.append(point)
        
        # 去重并排序
        filtered_split_points = sorted(list(set(filtered_split_points)))
        self.update_status(f"分析完成，共找到 {len(filtered_split_points)} 个分割点")
        return filtered_split_points
    
    def cut_single_segment(self, input_file, start_time, end_time, output_file):
        """切割单个音频片段"""
        split_cmd = [
            self.ffmpeg_path, "-hide_banner", "-loglevel", "error",
            "-i", input_file, "-vn",
            "-ss", str(start_time),
            "-acodec", "libmp3lame", "-q:a", "2"
        ]
        
        if end_time:
            split_cmd.extend(["-to", str(end_time)])
        split_cmd.append(output_file)
        
        # 执行ffmpeg命令时隐藏控制台窗口
        subprocess.run(
            split_cmd,
            creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
        )
        return os.path.exists(output_file)
    
    def audio_to_text(self, audio_file_path):
        """将音频文件转换为文本（使用multipart/form-data格式）"""
        try:
            with open(audio_file_path, 'rb') as audio_file:
                content_type = 'audio/mp3'
                files = {
                    'file': (os.path.basename(audio_file_path), audio_file, content_type)
                }
                
                data = {
                    'model': self.model_name,
                    'language': 'auto',
                    'response_format': 'text'
                }
                
                headers = {
                    "Authorization": f"Bearer {self.api_key}"
                }
                
                self.update_status(f"正在识别 {os.path.basename(audio_file_path)}...")
                response = requests.post(
                    self.api_endpoint, 
                    files=files, 
                    data=data, 
                    headers=headers
                )
                response.raise_for_status()
                
                return response.text
                
        except Exception as e:
            self.update_status(f"识别错误: {str(e)}")
            return None
    
    def update_status(self, message):
        """更新状态信息（在主线程中执行）"""
        def update():
            self.status_var.set(message)
        self.root.after(0, update)
    
    def append_result(self, text):
        """向结果区域添加文本（在主线程中执行）"""
        def append():
            if self.result_text:  # 增加检查
                self.result_text.config(state=tk.NORMAL)
                self.result_text.insert(tk.END, text + "\n\n")  # 移除了时间信息
                self.result_text.see(tk.END)  # 滚动到最后
                self.result_text.config(state=tk.DISABLED)
        self.root.after(0, append)
    
    def update_progress(self, value):
        """更新进度条（在主线程中执行）"""
        def update():
            self.progress_var.set(value)
        self.root.after(0, update)
    
    def process_audio_sequentially(self):
        """顺序处理：切割一个片段 → 识别 → 删除片段 → 再切割下一个"""
        try:
            # 创建输出目录
            os.makedirs(self.output_dir, exist_ok=True)
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            output_prefix = os.path.join(self.output_dir, f"segment_{timestamp}_")
            
            # 先获取所有分割点（仅分析）
            split_points = self.get_split_points(self.input_file)
            if len(split_points) < 1:
                self.update_status("未找到有效的分割点，无法继续处理")
                self.finish_processing()
                return
            
            # 顺序处理每个片段
            transcription_results = []
            segment_count = 1
            total_segments = len(split_points) - 1  # 总片段数
            
            self.update_status("开始顺序处理片段...")
            
            for i in range(len(split_points)):
                if not self.processing:  # 检查是否需要取消
                    break
                    
                start_time = split_points[i]
                
                # 确定结束时间
                if i < len(split_points) - 1:
                    end_time = split_points[i+1]
                    if end_time - start_time < 1.0:  # 跳过太短的片段
                        self.update_status(f"跳过太短的片段 (时长 < 1秒): {start_time:.2f}s 至 {end_time:.2f}s")
                        continue
                else:
                    end_time = None
                
                # 更新进度
                progress = (i / total_segments) * 100 if total_segments > 0 else 0
                self.update_progress(progress)
                
                # 1. 切割当前片段
                output_file = f"{output_prefix}{segment_count:03d}.mp3"
                self.update_status(f"切割片段 {segment_count}: {start_time:.2f}s 至 {end_time:.2f}s" 
                      if end_time else f"切割片段 {segment_count}: 从 {start_time:.2f}s 开始")
                
                if not self.cut_single_segment(self.input_file, start_time, end_time, output_file):
                    self.update_status(f"片段 {segment_count} 切割失败，跳过")
                    segment_count += 1
                    continue
                
                # 2. 立即识别当前片段
                text = self.audio_to_text(output_file)
                
                # 3. 无论识别成功与否，都删除音频片段
                try:
                    if os.path.exists(output_file):
                        os.remove(output_file)
                        self.update_status(f"已删除临时文件: {os.path.basename(output_file)}")
                except Exception as e:
                    self.update_status(f"删除临时文件失败: {str(e)}")
                
                if text:
                    self.update_status(f"片段 {segment_count} 识别成功")
                    self.append_result(text)  # 只添加文本内容，不包含时间信息
                    
                    transcription_results.append({
                        "segment": segment_count,
                        "start_time": start_time,
                        "end_time": end_time,
                        "text": text
                    })
                else:
                    self.update_status(f"片段 {segment_count} 识别失败")
                
                segment_count += 1
            
            # 更新进度为100%
            self.update_progress(100)
            
            # 保存所有结果
            result_file = os.path.join(self.output_dir, f"transcription_{timestamp}.json")
            with open(result_file, "w", encoding="utf-8") as f:
                json.dump(transcription_results, f, ensure_ascii=False, indent=2)
            
            text_file = os.path.join(self.output_dir, f"transcription_{timestamp}.txt")
            with open(text_file, "w", encoding="utf-8") as f:
                for item in transcription_results:
                    f.write(f"{item['text']}\n\n")  # 文本文件中也不包含时间信息
            
            self.update_status(f"所有处理完成，共处理 {segment_count - 1} 个片段")
            self.append_result(f"所有处理完成！结果已保存到:\n{text_file}")
            
            # 显示完成提示
            def show_complete_message():
                messagebox.showinfo("完成", f"所有处理已完成，共处理 {segment_count - 1} 个片段\n结果已保存到 {text_file}")
            self.root.after(0, show_complete_message)
            
        except Exception as e:
            self.update_status(f"处理过程中发生错误: {str(e)}")
            def show_error_message():
                messagebox.showerror("错误", f"处理过程中发生错误: {str(e)}")
            self.root.after(0, show_error_message)
        finally:
            self.finish_processing()
    
    def finish_processing(self):
        """完成处理，恢复界面状态"""
        self.processing = False
        # 恢复按钮状态
        def enable_buttons():
            for widget in self.root.winfo_children():
                if isinstance(widget, ttk.Button) and widget["text"] in ["开始处理", "浏览..."]:
                    widget.config(state=tk.NORMAL)
        self.root.after(0, enable_buttons)

if __name__ == "__main__":
    root = tk.Tk()
    app = MP4ToTextConverter(root)
    root.mainloop()
    
